#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
豆包音乐下载工具 v1.4
用于下载豆包音乐分享链接中的音乐
support普通模式和Selenium浏览器自动化模式，新增GUI界面支持
"""

import os
import re
import sys
import json
import time
import threading
import logging
import requests
import platform
from urllib.parse import urlparse, parse_qs
import argparse
from datetime import datetime
from logging.handlers import RotatingFileHandler

# 配置日志系统
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.StreamHandler(),
        RotatingFileHandler(
            'doubao_downloader.log',
            maxBytes=5*1024*1024,  # 5MB
            backupCount=3
        )
    ]
)
logger = logging.getLogger('doubao_downloader')

class Logger:
    """自定义日志类，提供更友好的日志输出和进度显示"""
    
    LEVELS = {
        'DEBUG': 10,
        'INFO': 20,
        'WARNING': 30,
        'ERROR': 40,
        'CRITICAL': 50
    }
    
    COLORS = {
        'DEBUG': '\033[94m',  # 蓝色
        'INFO': '\033[92m',   # 绿色
        'WARNING': '\033[93m', # 黄色
        'ERROR': '\033[91m',   # 红色
        'CRITICAL': '\033[95m', # 紫色
        'RESET': '\033[0m'
    }
    
    def __init__(self, level='INFO'):
        self.level = self.LEVELS.get(level, 20)
        self.last_progress_update = 0
    
    def _log(self, level_name, message):
        """输出日志信息"""
        if self.LEVELS.get(level_name, 20) >= self.level:
            timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            # 控制台彩色输出
            if sys.stdout.isatty() and os.name != 'nt':
                colored_message = f"{self.COLORS[level_name]}{timestamp} [{level_name}] {message}{self.COLORS['RESET']}"
                print(colored_message)
            else:
                # Windows不支持ANSI颜色，使用普通输出
                print(f"{timestamp} [{level_name}] {message}")
            
            # 写入日志文件（使用Python标准日志库）
            logger_func = getattr(logger, level_name.lower())
            logger_func(message)
    
    def debug(self, message):
        self._log('DEBUG', message)
    
    def info(self, message):
        self._log('INFO', message)
    
    def warning(self, message):
        self._log('WARNING', message)
    
    def error(self, message):
        self._log('ERROR', message)
    
    def critical(self, message):
        self._log('CRITICAL', message)
    
    def progress(self, current, total, file_name=""):
        """显示下载进度条"""
        if total > 0:
            # 限制进度条更新频率，避免刷新过快
            current_time = time.time()
            if current_time - self.last_progress_update < 0.1 and current < total:
                return
            
            self.last_progress_update = current_time
            
            # 计算进度百分比
            percentage = (current / total) * 100
            
            # 格式化进度显示
            bar_length = 30
            filled_length = int(bar_length * current // total)
            bar = '█' * filled_length + '-' * (bar_length - filled_length)
            
            # 格式化大小显示
            current_size = self.format_size(current)
            total_size = self.format_size(total)
            
            # 构建进度信息
            progress_info = f"[{bar}] {percentage:.1f}% | {current_size}/{total_size}"
            if file_name:
                progress_info = f"{file_name[:20]}... | {progress_info}"
            
            # 在同一行更新进度
            sys.stdout.write(f"\r{progress_info}")
            sys.stdout.flush()
    
    def format_size(self, size_bytes):
        """格式化文件大小显示"""
        for unit in ['B', 'KB', 'MB', 'GB']:
            if size_bytes < 1024 or unit == 'GB':
                return f"{size_bytes:.2f} {unit}"
            size_bytes /= 1024

# 创建全局日志实例
global_logger = Logger()

# 尝试导入PyQt5相关模块
try:
    # 尝试导入PyQt5组件，并捕获具体的导入错误
    print("正在尝试导入PyQt5...")
    from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, 
                                QHBoxLayout, QLabel, QLineEdit, QPushButton, 
                                QProgressBar, QTextEdit, QCheckBox, QMessageBox, 
                                QFileDialog, QGroupBox, QFormLayout)
    print("✅ PyQt5.QtWidgets导入成功")
    from PyQt5.QtCore import Qt, QThread, pyqtSignal
    print("✅ PyQt5.QtCore导入成功")
    from PyQt5.QtGui import QIcon, QFont, QTextOption, QIntValidator
    print("✅ PyQt5.QtGui导入成功")
    qt_available = True
    print("✅ PyQt5模块已成功导入")
except ImportError as e:
    qt_available = False
    print(f"❌ PyQt5导入失败: {str(e)}")
    print("   请尝试重新安装PyQt5: pip install PyQt5 --force-reinstall")
    # 即使导入失败，我们也要定义一些基本类以避免后续代码崩溃
    class QThread:
        pass
    class pyqtSignal:
        def __init__(self, *types):
            pass
        def connect(self, slot):
            pass
        def emit(self, *args):
            pass

# 尝试导入Selenium相关模块
try:
    from selenium import webdriver
    from selenium.webdriver.chrome.service import Service
    from selenium.webdriver.chrome.options import Options
    from selenium.webdriver.common.by import By
    from selenium.webdriver.support.ui import WebDriverWait
    from selenium.webdriver.support import expected_conditions as EC
    from selenium.common.exceptions import TimeoutException, WebDriverException
    selenium_available = True
    print("✅ Selenium模块已成功导入")
except ImportError as e:
    selenium_available = False
    print("⚠️ Selenium模块未安装，浏览器自动化功能将不可用")
    print("   如需使用浏览器自动化功能，请运行: pip install selenium")
    print("   并下载与您Chrome浏览器版本匹配的ChromeDriver")

# 设置请求头，模拟浏览器访问
HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
    'Connection': 'keep-alive',
}

def sanitize_filename(filename):
    """清理文件名中的非法字符"""
    # 替换Windows文件名中的非法字符
    invalid_chars = r'/:*?"<>|'
    for char in invalid_chars:
        filename = filename.replace(char, '_')
    # 替换控制字符
    filename = ''.join(char for char in filename if ord(char) > 31)
    # 限制文件名长度
    max_length = 200
    if len(filename) > max_length:
        name, ext = os.path.splitext(filename)
        name = name[:max_length - len(ext)]
        filename = name + ext
    return filename

class DownloadThread(QThread):
    """下载线程类，用于在后台执行下载任务"""
    
    # 定义信号
    update_progress = pyqtSignal(int)
    update_status = pyqtSignal(str)
    download_complete = pyqtSignal(bool, str)
    music_info_found = pyqtSignal(dict)
    update_download_speed = pyqtSignal(str)  # 新增下载速度信号
    update_estimated_time = pyqtSignal(str)  # 新增预估时间信号
    
    def __init__(self, share_url, save_path=None, use_selenium=False, max_retries=3):
        super().__init__()
        self.share_url = share_url
        self.save_path = save_path
        self.use_selenium = use_selenium
        self.max_retries = max_retries  # 最大重试次数
        self.is_running = True
        self._last_progress = 0  # 上一次进度值
        self._start_time = None  # 下载开始时间
        self._total_size = 0     # 文件总大小
        self._downloaded_bytes = 0  # 已下载字节数
        self._speeds = []  # 速度历史，用于计算平均速度
    
    def _create_unique_filename(self, music_info, output_dir):
        """创建唯一的文件名，避免冲突"""
        # 使用艺术家和标题创建基本文件名
        base_filename = f"{music_info['artist']} - {music_info['title']}"
        filename = sanitize_filename(f"{base_filename}.mp3")
        file_path = os.path.join(output_dir, filename)
        
        # 如果文件已存在，添加时间戳或序号
        if os.path.exists(file_path):
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            name, ext = os.path.splitext(filename)
            # 添加序号而非仅仅是时间戳，确保唯一性
            counter = 1
            new_filename = f"{name}_{timestamp}_{counter}{ext}"
            new_file_path = os.path.join(output_dir, new_filename)
            
            while os.path.exists(new_file_path):
                counter += 1
                new_filename = f"{name}_{timestamp}_{counter}{ext}"
                new_file_path = os.path.join(output_dir, new_filename)
            
            filename = new_filename
            file_path = new_file_path
            global_logger.info(f"文件已存在，使用新文件名: {filename}")
        
        return file_path
    
    def _update_progress_smoothly(self, current_progress):
        """平滑更新进度，避免进度条跳动"""
        if not self.is_running or current_progress < self._last_progress:
            return
            
        # 确保进度条平滑过渡
        if current_progress - self._last_progress > 5:  # 如果进度跳跃较大
            # 分步骤更新进度
            steps = min(5, current_progress - self._last_progress)  # 最多5步
            for i in range(1, steps + 1):
                progress = self._last_progress + (current_progress - self._last_progress) * i // steps
                self.update_progress.emit(progress)
                time.sleep(0.05)  # 短暂延迟确保UI有时间更新
        else:
            # 小幅度进度变化直接更新
            self.update_progress.emit(current_progress)
        
        self._last_progress = current_progress
    
    def _calculate_download_speed(self, downloaded_bytes):
        """计算下载速度，使用基于时间的滑动窗口"""
        if self._start_time is None:
            self._start_time = time.time()
            self._downloaded_bytes = downloaded_bytes
            return "0 MB/s"
            
        current_time = time.time()
        elapsed_time = current_time - self._start_time
        if elapsed_time < 0.1:  # 避免除以接近零的值
            return "计算中..."
            
        # 记录当前时间和已下载字节数，用于滑动窗口计算
        self._speeds.append((current_time, downloaded_bytes))
        
        # 保持30秒的滑动窗口，移除过期数据点
        window_size = 30  # 30秒
        self._speeds = [(t, b) for t, b in self._speeds if current_time - t <= window_size]
        
        # 至少需要两个数据点才能计算速度
        if len(self._speeds) >= 2:
            # 使用滑动窗口中的最早和最晚数据点计算平均速度
            oldest_time, oldest_bytes = self._speeds[0]
            latest_time, latest_bytes = self._speeds[-1]
            
            time_diff = max(0.1, latest_time - oldest_time)  # 避免除以非常小的值
            bytes_diff = latest_bytes - oldest_bytes
            
            if bytes_diff > 0:
                avg_speed = bytes_diff / time_diff
            else:
                avg_speed = 0
        else:
            # 数据点不足时回退到简单计算
            bytes_diff = downloaded_bytes - self._downloaded_bytes
            avg_speed = bytes_diff / max(0.1, elapsed_time)
        
        # 更新已下载字节数
        self._downloaded_bytes = downloaded_bytes
        
        # 格式化速度
        if avg_speed > 1024 * 1024:  # 大于1MB/s
            return f"{avg_speed / (1024 * 1024):.2f} MB/s"
        elif avg_speed > 1024:  # 大于1KB/s
            return f"{avg_speed / 1024:.2f} KB/s"
        else:
            return f"{avg_speed:.2f} B/s"
    
    def _calculate_estimated_time(self, downloaded_bytes):
        """计算预估剩余时间，使用更合理的算法"""
        if self._total_size <= 0 or downloaded_bytes <= 0:
            return "未知"
            
        # 基于滑动窗口数据点计算平均速度
        if len(self._speeds) >= 2:
            oldest_time, oldest_bytes = self._speeds[0]
            latest_time, latest_bytes = self._speeds[-1]
            
            time_diff = max(0.1, latest_time - oldest_time)
            bytes_diff = latest_bytes - oldest_bytes
            
            if bytes_diff > 0:
                avg_speed = bytes_diff / time_diff
            else:
                return "计算中..."
        else:
            # 数据点不足时，使用已下载总量计算平均速度
            elapsed_time = max(0.1, time.time() - self._start_time)
            avg_speed = downloaded_bytes / elapsed_time
        
        # 计算剩余字节数和剩余时间
        remaining_bytes = self._total_size - downloaded_bytes
        if avg_speed <= 0:
            return "未知"
        
        remaining_seconds = remaining_bytes / avg_speed
        
        # 限制预估时间，避免显示过长时间
        if remaining_seconds > 86400:  # 超过24小时
            return "预计超过24小时"
        
        # 格式化时间显示
        if remaining_seconds < 60:
            return f"{remaining_seconds:.1f}秒"
        elif remaining_seconds < 3600:
            minutes = remaining_seconds // 60
            seconds = remaining_seconds % 60
            return f"{minutes:.0f}分{seconds:.0f}秒"
        else:
            hours = remaining_seconds // 3600
            minutes = (remaining_seconds % 3600) // 60
            return f"{hours:.0f}时{minutes:.0f}分"
    
    def run(self):
        """
        线程运行函数
        """
        try:
            global_logger.info(f"开始下载任务: {self.share_url}")
            self.update_status.emit("🔍 正在解析分享链接...")
            self.update_progress.emit(0)  # 初始进度
            
            # 解析链接
            vid = parse_share_url(self.share_url)
            if not vid:
                self.update_status.emit("❌ 无法解析分享链接")
                self.download_complete.emit(False, "")
                return
            
            # 获取音乐信息
            self.update_status.emit("📝 正在获取音乐信息...")
            self.update_progress.emit(10)
            
            if self.use_selenium:
                # 使用Selenium模式获取音乐信息和下载
                music_info = get_music_info_with_selenium(vid)
                if music_info:
                    self.music_info_found.emit(music_info)
                    self.update_status.emit("🎵 音乐信息获取成功")
                    self.update_progress.emit(30)
                    
                    # 创建保存目录
                    if not self.save_path:
                        self.save_path = os.path.join(os.getcwd(), 'downloads')
                    
                    if not os.path.isdir(self.save_path):
                        os.makedirs(self.save_path, exist_ok=True)
                    
                    # 创建唯一文件名
                    file_path = self._create_unique_filename(music_info, self.save_path)
                    
                    # 下载文件
                    self.update_status.emit("🚀 正在下载音乐文件...")
                    self.update_progress.emit(40)
                    
                    # 使用Selenium下载
                    success = download_music_with_selenium(self.share_url, file_path)
                    if success:
                        self.update_status.emit("✅ 下载完成")
                        self.update_progress.emit(100)
                        self.download_complete.emit(True, file_path)
                    else:
                        self.update_status.emit("❌ Selenium下载失败")
                        self.download_complete.emit(False, "")
                else:
                    self.update_status.emit("❌ 无法获取音乐信息")
                    self.download_complete.emit(False, "")
            else:
                # 使用普通模式获取音乐信息和下载
                for retry in range(self.max_retries):
                    music_info = get_music_info(vid)
                    if music_info:
                        self.music_info_found.emit(music_info)
                        self.update_status.emit("🎵 音乐信息获取成功")
                        self.update_progress.emit(30)
                        
                        # 创建保存目录
                        if not self.save_path:
                            self.save_path = os.path.join(os.getcwd(), 'downloads')
                        
                        if not os.path.isdir(self.save_path):
                            os.makedirs(self.save_path, exist_ok=True)
                        
                        # 创建唯一文件名
                        file_path = self._create_unique_filename(music_info, self.save_path)
                        
                        # 下载文件
                        self.update_status.emit("🚀 正在下载音乐文件...")
                        self.update_progress.emit(40)
                        
                        # 尝试下载文件
                        success = download_music_file(music_info['audio_url'], file_path, self)
                        if success:
                            self.update_status.emit("✅ 下载完成")
                            self.update_progress.emit(100)
                            self.download_complete.emit(True, file_path)
                            break
                        else:
                            if retry < self.max_retries - 1:
                                self.update_status.emit(f"⚠️ 下载失败，第{retry + 2}次尝试中...")
                                time.sleep(2)  # 等待2秒后重试
                            else:
                                self.update_status.emit("❌ 下载失败，已达到最大重试次数")
                                self.download_complete.emit(False, "")
                    else:
                        if retry < self.max_retries - 1:
                            self.update_status.emit(f"⚠️ 获取音乐信息失败，第{retry + 2}次尝试中...")
                            time.sleep(2)  # 等待2秒后重试
                        else:
                            self.update_status.emit("❌ 无法获取音乐信息，已达到最大重试次数")
                            self.download_complete.emit(False, "")
        except Exception as e:
            global_logger.error(f"下载过程中发生错误: {str(e)}")
            import traceback
            global_logger.debug(traceback.format_exc())
            self.update_status.emit(f"❌ 下载异常: {str(e)}")
            self.download_complete.emit(False, "")
    
    def stop(self):
        """停止下载任务"""
        self.is_running = False
        self.wait()

class MusicDownloaderGUI(QMainWindow):
    """豆包音乐下载器图形界面类"""
    
    def __init__(self):
        super().__init__()
        self.setWindowTitle("豆包音乐下载器 v1.4")
        self.setGeometry(100, 100, 800, 600)
        self.download_thread = None
        self.init_ui()
    
    def init_ui(self):
        """初始化用户界面"""
        # 设置主布局
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        main_layout = QVBoxLayout(central_widget)
        
        # 创建输入区域
        input_group = QGroupBox("输入信息")
        input_layout = QFormLayout()
        
        # 分享链接输入框
        self.url_input = QLineEdit()
        self.url_input.setPlaceholderText("请输入豆包音乐分享链接")
        self.url_input.setMinimumHeight(30)
        input_layout.addRow("分享链接:", self.url_input)
        
        # 保存路径选择
        path_layout = QHBoxLayout()
        self.path_input = QLineEdit()
        self.path_input.setPlaceholderText("选择保存目录")
        self.path_input.setMinimumHeight(30)
        self.path_input.setText(os.path.join(os.getcwd(), 'downloads'))
        
        browse_button = QPushButton("浏览...")
        browse_button.clicked.connect(self.on_browse_clicked)
        browse_button.setMinimumHeight(30)
        
        path_layout.addWidget(self.path_input)
        path_layout.addWidget(browse_button)
        input_layout.addRow("保存目录:", path_layout)
        
        # 选项设置
        options_layout = QHBoxLayout()
        
        # 使用Selenium模式复选框
        self.selenium_checkbox = QCheckBox("使用Selenium模式")
        if not selenium_available:
            self.selenium_checkbox.setEnabled(False)
            self.selenium_checkbox.setToolTip("Selenium模块未安装")
        options_layout.addWidget(self.selenium_checkbox)
        
        # 重试次数选择
        retry_layout = QHBoxLayout()
        retry_label = QLabel("重试次数:")
        self.retry_input = QLineEdit("3")
        self.retry_input.setMaximumWidth(60)
        self.retry_input.setValidator(QIntValidator(1, 10))
        retry_layout.addWidget(retry_label)
        retry_layout.addWidget(self.retry_input)
        options_layout.addLayout(retry_layout)
        
        options_layout.addStretch()
        input_layout.addRow(options_layout)
        
        # 添加输入区域到主布局
        input_group.setLayout(input_layout)
        main_layout.addWidget(input_group)
        
        # 创建按钮区域
        button_layout = QHBoxLayout()
        
        # 解析按钮
        parse_button = QPushButton("📝 解析链接")
        parse_button.clicked.connect(self.on_parse_clicked)
        parse_button.setMinimumHeight(40)
        button_layout.addWidget(parse_button)
        
        # 下载按钮
        self.download_button = QPushButton("🚀 开始下载")
        self.download_button.clicked.connect(self.on_download_clicked)
        self.download_button.setMinimumHeight(40)
        self.download_button.setEnabled(False)
        button_layout.addWidget(self.download_button)
        
        # 清理按钮
        clear_button = QPushButton("🗑️ 清理")
        clear_button.clicked.connect(self.on_clear_clicked)
        clear_button.setMinimumHeight(40)
        button_layout.addWidget(clear_button)
        
        # 添加按钮区域到主布局
        main_layout.addLayout(button_layout)
        
        # 创建音乐信息区域
        info_group = QGroupBox("音乐信息")
        info_layout = QFormLayout()
        
        # 音乐标题
        self.title_input = QLineEdit()
        self.title_input.setReadOnly(True)
        self.title_input.setMinimumHeight(25)
        info_layout.addRow("标题:", self.title_input)
        
        # 艺术家
        self.artist_input = QLineEdit()
        self.artist_input.setReadOnly(True)
        self.artist_input.setMinimumHeight(25)
        info_layout.addRow("艺术家:", self.artist_input)
        
        # 时长
        self.duration_input = QLineEdit()
        self.duration_input.setReadOnly(True)
        self.duration_input.setMinimumHeight(25)
        info_layout.addRow("时长:", self.duration_input)
        
        # 文件大小
        self.size_input = QLineEdit()
        self.size_input.setReadOnly(True)
        self.size_input.setMinimumHeight(25)
        info_layout.addRow("文件大小:", self.size_input)
        
        # 添加音乐信息区域到主布局
        info_group.setLayout(info_layout)
        main_layout.addWidget(info_group)
        
        # 创建进度区域
        progress_group = QGroupBox("下载进度")
        progress_layout = QVBoxLayout()
        
        # 进度条
        self.progress_bar = QProgressBar()
        self.progress_bar.setValue(0)
        self.progress_bar.setTextVisible(True)
        self.progress_bar.setMinimumHeight(25)
        progress_layout.addWidget(self.progress_bar)
        
        # 状态信息
        status_layout = QHBoxLayout()
        
        # 状态标签
        self.status_label = QLabel("就绪")
        status_layout.addWidget(self.status_label)
        
        # 下载速度
        self.speed_label = QLabel("速度: --")
        status_layout.addWidget(self.speed_label)
        
        # 预计时间
        self.time_label = QLabel("剩余: --")
        status_layout.addWidget(self.time_label)
        
        status_layout.addStretch()
        progress_layout.addLayout(status_layout)
        
        # 添加进度区域到主布局
        progress_group.setLayout(progress_layout)
        main_layout.addWidget(progress_group)
        
        # 创建日志区域
        log_group = QGroupBox("日志信息")
        log_layout = QVBoxLayout()
        
        # 日志文本框
        self.log_text = QTextEdit()
        self.log_text.setReadOnly(True)
        self.log_text.setLineWrapMode(QTextEdit.WidgetWidth)
        self.log_text.setWordWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere)
        log_layout.addWidget(self.log_text)
        
        # 添加日志区域到主布局
        log_group.setLayout(log_layout)
        main_layout.addWidget(log_group)
        
        # 添加状态栏
        self.statusBar().showMessage("就绪")
        
        # 添加快捷键
        self.url_input.returnPressed.connect(self.on_parse_clicked)
    
    def on_browse_clicked(self):
        """浏览保存目录"""
        directory = QFileDialog.getExistingDirectory(self, "选择保存目录", self.path_input.text())
        if directory:
            self.path_input.setText(directory)
    
    def on_parse_clicked(self):
        """解析分享链接"""
        url = self.url_input.text().strip()
        if not url:
            QMessageBox.warning(self, "警告", "请输入豆包音乐分享链接")
            return
        
        try:
            self.status_label.setText("🔍 正在解析链接...")
            self.statusBar().showMessage("正在解析链接...")
            
            # 解析视频ID
            vid = parse_share_url(url)
            if not vid:
                QMessageBox.warning(self, "解析失败", "无法解析分享链接，请检查链接格式是否正确")
                self.status_label.setText("解析失败")
                self.statusBar().showMessage("解析失败")
                return
            
            # 获取音乐信息
            self.status_label.setText("📝 正在获取音乐信息...")
            self.statusBar().showMessage("正在获取音乐信息...")
            
            use_selenium = self.selenium_checkbox.isChecked()
            if use_selenium and selenium_available:
                music_info = get_music_info_with_selenium(vid)
            else:
                music_info = get_music_info(vid)
            
            if music_info:
                # 显示音乐信息
                self.title_input.setText(music_info['title'])
                self.artist_input.setText(music_info['artist'])
                self.duration_input.setText(music_info['duration'])
                self.size_input.setText(music_info['size'])
                
                self.status_label.setText("✅ 解析成功")
                self.statusBar().showMessage("解析成功")
                self.download_button.setEnabled(True)
                
                # 添加到日志
                self.log_text.append(f"[{datetime.now().strftime('%H:%M:%S')}] 🔍 解析成功: {music_info['artist']} - {music_info['title']}")
            else:
                QMessageBox.warning(self, "获取失败", "无法获取音乐信息，请检查链接是否有效")
                self.status_label.setText("获取失败")
                self.statusBar().showMessage("获取失败")
                self.download_button.setEnabled(False)
        except Exception as e:
            QMessageBox.critical(self, "错误", f"解析过程中发生错误: {str(e)}")
            self.status_label.setText("解析错误")
            self.statusBar().showMessage("解析错误")
    
    def on_download_clicked(self):
        """开始下载音乐"""
        url = self.url_input.text().strip()
        save_path = self.path_input.text().strip()
        
        if not url:
            QMessageBox.warning(self, "警告", "请输入豆包音乐分享链接")
            return
        
        if not save_path:
            QMessageBox.warning(self, "警告", "请选择保存目录")
            return
        
        try:
            # 创建保存目录
            os.makedirs(save_path, exist_ok=True)
            
            # 获取重试次数
            try:
                max_retries = int(self.retry_input.text())
                if max_retries < 1 or max_retries > 10:
                    max_retries = 3
            except:
                max_retries = 3
            
            # 创建下载线程
            use_selenium = self.selenium_checkbox.isChecked()
            self.download_thread = DownloadThread(url, save_path, use_selenium, max_retries)
            
            # 连接信号
            self.download_thread.update_progress.connect(self.on_progress_update)
            self.download_thread.update_status.connect(self.status_label.setText)
            self.download_thread.download_complete.connect(self.on_download_complete)
            self.download_thread.music_info_found.connect(self.on_music_info_found)
            self.download_thread.update_download_speed.connect(self.on_download_speed_update)
            self.download_thread.update_estimated_time.connect(self.on_estimated_time_update)
            
            # 禁用按钮
            self.download_button.setEnabled(False)
            
            # 启动下载线程
            self.download_thread.start()
            
            # 添加到日志
            self.log_text.append(f"[{datetime.now().strftime('%H:%M:%S')}] 🚀 开始下载: {url}")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"启动下载失败: {str(e)}")
    
    def on_progress_update(self, progress):
        """更新下载进度"""
        self.progress_bar.setValue(progress)
    
    def on_music_info_found(self, music_info):
        """更新音乐信息"""
        self.title_input.setText(music_info['title'])
        self.artist_input.setText(music_info['artist'])
        self.duration_input.setText(music_info['duration'])
        self.size_input.setText(music_info['size'])
        
        # 添加到日志
        self.log_text.append(f"[{datetime.now().strftime('%H:%M:%S')}] 🎵 音乐信息: {music_info['artist']} - {music_info['title']}")
    
    def on_download_speed_update(self, speed):
        """更新下载速度"""
        self.speed_label.setText(f"速度: {speed}")
    
    def on_estimated_time_update(self, estimated_time):
        """更新预计时间"""
        self.time_label.setText(f"剩余: {estimated_time}")
    
    def on_download_complete(self, success, file_path):
        """下载完成处理"""
        if success:
            self.status_label.setText("✅ 下载完成")
            self.statusBar().showMessage("下载完成")
            
            # 添加到日志
            self.log_text.append(f"[{datetime.now().strftime('%H:%M:%S')}] ✅ 下载完成: {os.path.basename(file_path)}")
            self.log_text.append(f"[{datetime.now().strftime('%H:%M:%S')}] 📁 保存路径: {file_path}")
            
            # 显示成功消息
            reply = QMessageBox.information(self, "下载成功", f"音乐已成功下载到:\n{file_path}\n\n是否打开文件?", QMessageBox.Yes | QMessageBox.No)
            if reply == QMessageBox.Yes:
                # 打开文件
                if os.name == 'nt':  # Windows
                    os.startfile(file_path)
                elif os.name == 'posix':  # macOS, Linux
                    import subprocess
                    subprocess.call(['open', file_path] if platform.system() == 'Darwin' else ['xdg-open', file_path])
        else:
            self.status_label.setText("❌ 下载失败")
            self.statusBar().showMessage("下载失败")
            
            # 添加到日志
            self.log_text.append(f"[{datetime.now().strftime('%H:%M:%S')}] ❌ 下载失败")
            
            # 显示失败消息
            QMessageBox.warning(self, "下载失败", "音乐下载失败，请检查链接是否有效或网络连接是否正常")
        
        # 重新启用下载按钮
        self.download_button.setEnabled(True)
    
    def on_clear_clicked(self):
        """清理输入和信息"""
        # 清除输入
        self.url_input.clear()
        
        # 清除音乐信息
        self.title_input.clear()
        self.artist_input.clear()
        self.duration_input.clear()
        self.size_input.clear()
        
        # 重置进度
        self.progress_bar.setValue(0)
        self.status_label.setText("就绪")
        self.speed_label.setText("速度: --")
        self.time_label.setText("剩余: --")
        
        # 重置状态栏
        self.statusBar().showMessage("就绪")
        
        # 禁用下载按钮
        self.download_button.setEnabled(False)
    
    def on_about_clicked(self):
        """显示关于对话框"""
        QMessageBox.about(self, "关于", "豆包音乐下载器 v1.4\n\n用于下载豆包音乐分享链接中的音乐\n\n支持普通模式和Selenium浏览器自动化模式\n\n作者: AI Assistant")
    
    def show_guide_dialog(self, vid=None, music_info=None):
        """显示使用向导对话框"""
        # 这里可以实现一个使用向导对话框
        pass

def parse_share_url(share_url):
    """解析豆包音乐分享链接，提取视频ID"""
    try:
        # 解析URL
        parsed_url = urlparse(share_url)
        
        # 检查是否为豆包音乐分享链接
        if 'doubao.com' not in parsed_url.netloc or 'music-sharing' not in parsed_url.path:
            global_logger.warning("无效的豆包音乐分享链接")
            return None
        
        # 提取查询参数
        query_params = parse_qs(parsed_url.query)
        
        # 获取视频ID
        if 'vid' in query_params:
            vid = query_params['vid'][0]
            global_logger.info(f"成功解析视频ID: {vid}")
            return vid
        else:
            # 尝试从URL路径中提取
            path_parts = parsed_url.path.split('/')
            for part in path_parts:
                if part and len(part) > 10 and 'vid=' in part:
                    # 尝试从路径中提取vid参数
                    vid_match = re.search(r'vid=([^&]+)', part)
                    if vid_match:
                        vid = vid_match.group(1)
                        global_logger.info(f"成功解析视频ID: {vid}")
                        return vid
            
            global_logger.warning("无法从链接中提取视频ID")
            return None
    except Exception as e:
        global_logger.error(f"解析分享链接时出错: {str(e)}")
        return None

def get_music_info(vid):
    """获取音乐信息"""
    try:
        # 构建API请求URL
        api_url = f"https://www.doubao.com/music-sharing/api/share-info?vid={vid}"
        
        # 发送请求
        response = requests.get(api_url, headers=HEADERS, timeout=10)
        
        # 检查响应状态
        if response.status_code == 200:
            # 解析响应数据
            data = response.json()
            
            # 检查响应是否成功
            if data.get('ok') and data.get('data'):
                music_data = data['data']
                
                # 提取音乐信息
                music_info = {
                    'title': music_data.get('title', '未知标题'),
                    'artist': music_data.get('author', '未知艺术家'),
                    'duration': format_duration(music_data.get('duration', 0)),
                    'size': '未知',  # 需要在下载时获取
                    'audio_url': music_data.get('audio_url')
                }
                
                # 验证是否有音频URL
                if not music_info['audio_url']:
                    global_logger.warning("音乐信息中没有找到音频URL")
                    return None
                
                global_logger.info(f"成功获取音乐信息: {music_info['artist']} - {music_info['title']}")
                return music_info
            else:
                global_logger.warning(f"API返回错误: {data.get('error_msg', '未知错误')}")
                return None
        else:
            global_logger.warning(f"API请求失败，状态码: {response.status_code}")
            return None
    except Exception as e:
        global_logger.error(f"获取音乐信息时出错: {str(e)}")
        return None

def get_music_info_with_selenium(vid):
    """使用Selenium获取音乐信息"""
    try:
        # 检查Selenium是否可用
        if not selenium_available:
            global_logger.warning("Selenium模块未安装")
            return None
        
        # 设置Chrome驱动选项
        options = setup_chrome_driver()
        if not options:
            return None
        
        # 创建WebDriver实例
        driver = webdriver.Chrome(options=options)
        driver.set_page_load_timeout(30)
        
        # 构建分享页面URL
        share_url = f"https://www.doubao.com/music-sharing?vid={vid}"
        
        # 访问页面
        global_logger.info(f"正在访问分享页面: {share_url}")
        driver.get(share_url)
        
        # 等待页面加载
        WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.TAG_NAME, 'body'))
        )
        
        # 尝试获取音乐信息
        title = "未知标题"
        artist = "未知艺术家"
        duration = "00:00"
        audio_url = None
        
        try:
            # 获取标题
            title_element = driver.find_element(By.CLASS_NAME, 'title')
            if title_element:
                title = title_element.text.strip()
        except:
            global_logger.warning("无法获取音乐标题")
        
        try:
            # 获取艺术家
            artist_element = driver.find_element(By.CLASS_NAME, 'artist')
            if artist_element:
                artist = artist_element.text.strip()
        except:
            global_logger.warning("无法获取艺术家信息")
        
        try:
            # 获取音频元素
            audio_element = driver.find_element(By.TAG_NAME, 'audio')
            if audio_element and audio_element.get_attribute('src'):
                audio_url = audio_element.get_attribute('src')
                
                # 尝试获取时长
                try:
                    driver.execute_script("return arguments[0].duration", audio_element)
                    time.sleep(0.5)  # 等待JavaScript执行
                    duration_value = driver.execute_script("return arguments[0].duration", audio_element)
                    if duration_value and duration_value > 0:
                        duration = format_duration(duration_value)
                except:
                    pass
        except:
            global_logger.warning("无法获取音频元素")
        
        # 关闭浏览器
        driver.quit()
        
        # 验证是否有音频URL
        if not audio_url:
            global_logger.warning("Selenium模式下未找到音频URL")
            return None
        
        # 构建音乐信息
        music_info = {
            'title': title,
            'artist': artist,
            'duration': duration,
            'size': '未知',  # 需要在下载时获取
            'audio_url': audio_url
        }
        
        global_logger.info(f"Selenium模式下成功获取音乐信息: {music_info['artist']} - {music_info['title']}")
        return music_info
    except Exception as e:
        global_logger.error(f"Selenium获取音乐信息时出错: {str(e)}")
        import traceback
        global_logger.debug(traceback.format_exc())
        return None

def download_music_file(audio_url, save_path, thread=None):
    """下载音乐文件"""
    try:
        global_logger.info(f"开始下载音乐文件: {audio_url}")
        global_logger.info(f"保存路径: {save_path}")
        
        # 发送请求
        with requests.get(audio_url, stream=True, headers=HEADERS, timeout=30) as r:
            # 检查响应状态
            r.raise_for_status()
            
            # 获取文件大小
            total_size = int(r.headers.get('content-length', 0))
            if thread:
                thread._total_size = total_size
            
            # 创建保存目录
            os.makedirs(os.path.dirname(save_path), exist_ok=True)
            
            # 下载文件
            downloaded_size = 0
            with open(save_path, 'wb') as f:
                for chunk in r.iter_content(chunk_size=8192):
                    if not chunk:
                        continue
                    
                    # 写入文件
                    f.write(chunk)
                    downloaded_size += len(chunk)
                    
                    # 更新进度
                    if total_size > 0:
                        progress = int((downloaded_size / total_size) * 100)
                        if thread:
                            thread._update_progress_smoothly(progress)
                            thread.update_download_speed.emit(thread._calculate_download_speed(downloaded_size))
                            thread.update_estimated_time.emit(thread._calculate_estimated_time(downloaded_size))
                        else:
                            global_logger.progress(downloaded_size, total_size, os.path.basename(save_path))
                    
                    # 检查是否需要停止下载
                    if thread and not thread.is_running:
                        global_logger.warning("下载已取消")
                        # 尝试删除不完整的文件
                        try:
                            if os.path.exists(save_path):
                                os.remove(save_path)
                        except:
                            pass
                        return False
            
            # 确保进度显示为100%
            if thread:
                thread.update_progress.emit(100)
            else:
                global_logger.progress(downloaded_size, total_size, os.path.basename(save_path))
                print()  # 换行
            
            # 验证文件大小
            if total_size > 0 and downloaded_size != total_size:
                global_logger.warning(f"文件大小不匹配，预期: {total_size}，实际: {downloaded_size}")
                return False
            
            global_logger.info(f"音乐文件下载成功: {save_path}")
            return True
    except Exception as e:
        global_logger.error(f"下载音乐文件时出错: {str(e)}")
        import traceback
        global_logger.debug(traceback.format_exc())
        
        # 尝试删除不完整的文件
        try:
            if os.path.exists(save_path):
                os.remove(save_path)
        except:
            pass
        
        return False

def download_music_with_selenium(share_url, save_path):
    """使用Selenium下载音乐"""
    try:
        # 检查Selenium是否可用
        if not selenium_available:
            global_logger.warning("Selenium模块未安装")
            return False
        
        # 设置Chrome驱动选项
        options = setup_chrome_driver()
        if not options:
            return False
        
        # 创建WebDriver实例
        driver = webdriver.Chrome(options=options)
        driver.set_page_load_timeout(30)
        
        # 访问页面
        global_logger.info(f"Selenium模式访问: {share_url}")
        driver.get(share_url)
        
        # 等待音频元素加载
        WebDriverWait(driver, 15).until(
            EC.presence_of_element_located((By.TAG_NAME, 'audio'))
        )
        
        # 获取音频元素
        audio_elements = driver.find_elements(By.TAG_NAME, 'audio')
        if audio_elements:
            audio_url = audio_elements[0].get_attribute('src')
            if audio_url:
                global_logger.info(f"找到音频元素，URL: {audio_url}")
                # 使用requests下载
                with requests.get(audio_url, stream=True, timeout=30) as r:
                    r.raise_for_status()
                    with open(save_path, 'wb') as f:
                        for chunk in r.iter_content(chunk_size=8192):
                            if chunk:
                                f.write(chunk)
                
                global_logger.info(f"Selenium模式下载成功: {save_path}")
                driver.quit()
                return True
            else:
                global_logger.error("音频元素存在但没有src属性")
        else:
            global_logger.error("页面中没有找到音频元素")
        
        driver.quit()
        return False
    except Exception as e:
        global_logger.error(f"Selenium下载错误: {str(e)}")
        import traceback
        global_logger.debug(traceback.format_exc())
        return False

def generate_tampermonkey_script():
    """
    生成Tampermonkey浏览器脚本
    
    Returns:
        str: 脚本保存路径
    """
    script_content = '''// ==UserScript==
// @name         豆包音乐提取器
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  从豆包音乐分享页面提取音频URL并提供下载
// @author       You
// @match        https://www.doubao.com/music-sharing?*
// @icon         
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    
    // 创建下载按钮
    function createDownloadButton() {
        const button = document.createElement('button');
        button.textContent = '下载音频';
        button.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            padding: 10px 20px;
            background-color: #007bff;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
            z-index: 9999;
            box-shadow: 0 2px 10px rgba(0,0,0,0.2);
        `;
        
        button.addEventListener('click', function() {
            extractAudio();
        });
        
        document.body.appendChild(button);
    }
    
    // 提取音频
    function extractAudio() {
        const audio = document.querySelector('audio');
        if (audio && audio.src) {
            // 获取页面信息
            const title = document.querySelector('.title')?.textContent || '豆包音乐';
            const artist = document.querySelector('.artist')?.textContent || '未知艺术家';
            
            // 清理文件名
            const cleanTitle = title.replace(/[\\/:*?"<>|]/g, '_');
            const cleanArtist = artist.replace(/[\\/:*?"<>|]/g, '_');
            const filename = `${cleanArtist} - ${cleanTitle}.mp3`;
            
            // 下载音频
            downloadFile(audio.src, filename);
            
            // 显示信息
            alert(`音频URL: ${audio.src}\n将保存为: ${filename}`);
        } else {
            alert('未找到音频元素，请确保页面已完全加载');
        }
    }
    
    // 下载文件
    function downloadFile(url, filename) {
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
    }
    
    // 等待页面加载完成
    setTimeout(() => {
        createDownloadButton();
    }, 1000);
})();'''
    
    script_path = os.path.join(os.getcwd(), '豆包音乐提取器_tampermonkey.js')
    try:
        with open(script_path, 'w', encoding='utf-8') as f:
            f.write(script_content)
        global_logger.info(f"Tampermonkey脚本已生成: {script_path}")
        return script_path
    except Exception as e:
        global_logger.error(f"生成Tampermonkey脚本失败: {str(e)}")
        return None

def setup_chrome_driver(headless=False):
    """
    设置Chrome浏览器驱动
    """
    try:
        options = Options()
        
        # 禁用自动化控制标志，避免被网站检测
        options.add_experimental_option("excludeSwitches", ["enable-automation"])
        options.add_experimental_option("useAutomationExtension", False)
        options.add_argument("--disable-blink-features=AutomationControlled")
        
        # 设置用户代理，模拟真实浏览器
        user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
        options.add_argument(f"user-agent={user_agent}")
        
        # 设置无头模式
        if headless:
            options.add_argument("--headless")
            options.add_argument("--disable-gpu")
            options.add_argument("--window-size=1920,1080")
        
        # 禁用图片加载以提高速度
        prefs = {"profile.managed_default_content_settings.images": 2}
        options.add_experimental_option("prefs", prefs)
        
        # 设置其他Chrome选项
        options.add_argument("--no-sandbox")
        options.add_argument("--disable-dev-shm-usage")
        
        return options
    except Exception as e:
        global_logger.error(f"创建Chrome选项时出错: {str(e)}")
        return None

def format_duration(seconds):
    """格式化时长为 mm:ss 格式"""
    try:
        seconds = float(seconds)
        minutes = int(seconds // 60)
        seconds = int(seconds % 60)
        return f"{minutes:02d}:{seconds:02d}"
    except:
        return "00:00"

if __name__ == "__main__":
    """程序主入口"""
    if qt_available:
        app = QApplication(sys.argv)
        # 设置应用程序样式
        app.setStyle("Fusion")
        
        # 创建并显示主窗口
        window = MusicDownloaderGUI()
        window.show()
        
        # 运行应用程序
        sys.exit(app.exec_())
    else:
        print("❌ 无法启动图形界面，PyQt5模块不可用")
        print("请安装PyQt5后重试: pip install PyQt5")
        sys.exit(1)